1 /**
2 Copyright: Copyright (c) 2018, Joakim Brännström. All rights reserved.
3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This file contains an analyzer that uses clang-tidy.
7 */
8 module code_checker.engine.builtin.clang_tidy;
9 
10 import std.typecons : Tuple;
11 import std.exception : collectException;
12 import std.concurrency : Tid, thisTid;
13 import logger = std.experimental.logger;
14 
15 import code_checker.engine.builtin.clang_tidy_classification : CountErrorsResult;
16 import code_checker.engine.types;
17 import code_checker.process : RunResult;
18 import code_checker.types;
19 
20 @safe:
21 
22 class ClangTidy : BaseFixture {
23     private {
24         Environment env;
25         Result result_;
26         string[] tidyArgs;
27     }
28 
29     override string explain() {
30         return "using clang-tidy";
31     }
32 
33     /// The environment the analyzers execute in.
34     override void putEnv(Environment v) {
35         this.env = v;
36     }
37 
38     /// Setup the environment for analyze.
39     override void setup() {
40         import std.algorithm;
41         import std.array : appender, array;
42         import std.ascii;
43         import std.file : exists;
44         import std.range : put;
45         import std.format : format;
46         import code_checker.engine.builtin.clang_tidy_classification : filterSeverity;
47 
48         auto app = appender!(string[])();
49         app.put(env.clangTidy.binary);
50 
51         app.put("-p=.");
52 
53         if (env.clangTidy.applyFixit) {
54             app.put(["-fix"]);
55         } else if (env.clangTidy.applyFixitErrors) {
56             app.put(["-fix-errors"]);
57         } else {
58             app.put("-warnings-as-errors=*");
59         }
60 
61         env.compiler.extraFlags.map!(a => ["-extra-arg", a]).joiner.copy(app);
62 
63         ["-header-filter", env.clangTidy.headerFilter].copy(app);
64 
65         // inactivate those that are below the configured severity level.
66         // dfmt off
67         env.clangTidy.checks ~=
68             filterSeverity!(a => a < env.staticCode.severity)
69             .map!(a => format("-%s", a))
70             .array;
71         // dfmt on
72 
73         if (exists(ClangTidyConstants.confFile)) {
74             logger.infof("Using clang-tidy settings from the local '%s'",
75                     ClangTidyConstants.confFile);
76         } else {
77             logger.trace("Using config from the TOML file");
78 
79             auto c = appender!string();
80             c.put(`{Checks: "`);
81             env.clangTidy.checks.joiner(",").copy(c);
82             c.put(`",`);
83             c.put("CheckOptions: [");
84             env.clangTidy.options.joiner(",").copy(c);
85             c.put("]");
86             c.put("}");
87 
88             app.put("-config");
89             app.put(c.data);
90         }
91 
92         tidyArgs = app.data;
93     }
94 
95     /// Execute the analyzer.
96     override void execute() {
97         if (env.clangTidy.applyFixit || env.clangTidy.applyFixitErrors) {
98             executeFixit(env, tidyArgs, result_);
99         } else {
100             executeParallel(env, tidyArgs, result_);
101         }
102     }
103 
104     /// Cleanup after analyze.
105     override void tearDown() {
106     }
107 
108     /// Returns: the result of the analyzer.
109     override Result result() {
110         return result_;
111     }
112 }
113 
114 void executeParallel(Environment env, string[] tidyArgs, ref Result result_) {
115     import core.time : dur;
116     import std.concurrency : Tid, thisTid, receiveTimeout;
117     import std.format : format;
118     import std.parallelism : task, TaskPool;
119     import code_checker.compile_db : UserFileRange, parseFlag,
120         CompileCommandFilter, SearchResult;
121     import code_checker.engine.logger : Logger;
122 
123     bool logged_failure;
124     auto logg = Logger(env.logg.dir);
125 
126     void handleResult(immutable(TidyResult)* res_) @trusted nothrow {
127         import std.array : appender;
128         import std.format : format;
129         import std.typecons : nullableRef;
130         import colorize : Color, color, Background, Mode;
131         import code_checker.engine.builtin.clang_tidy_classification : mapClangTidy;
132 
133         auto res = nullableRef(cast() res_);
134 
135         logger.infof("%s '%s'", "clang-tidy analyzing".color(Color.yellow,
136                 Background.black), res.file).collectException;
137 
138         result_.score += res.clangTidyStatus == 0 ? 1 : res.errors.score;
139 
140         if (res.clangTidyStatus != 0) {
141             res.print;
142 
143             if (env.logg.toFile) {
144                 try {
145                     logg.put(res.file, [res.output]);
146                 } catch (Exception e) {
147                     logger.warning(e.msg).collectException;
148                     logger.warning("Unable to log to file").collectException;
149                 }
150             }
151 
152             if (!logged_failure) {
153                 result_.msg ~= Msg(MsgSeverity.failReason, "clang-tidy warn about file(s)");
154                 logged_failure = true;
155             }
156 
157             try {
158                 result_.msg ~= Msg(MsgSeverity.improveSuggestion,
159                         format("clang-tidy: %-(%s, %) in %s", res.errors.toRange, res.file));
160             } catch (Exception e) {
161                 logger.warning(e.msg).collectException;
162                 logger.warning("Unable to add user message to the result").collectException;
163             }
164         }
165 
166         result_.status = mergeStatus(result_.status, res.clangTidyStatus == 0
167                 ? Status.passed : Status.failed);
168         logger.trace(result_).collectException;
169     }
170 
171     auto pool = new TaskPool;
172     scope (exit)
173         pool.finish;
174 
175     static struct DoneCondition {
176         int expected;
177         int replies;
178 
179         bool isWaitingForReplies() {
180             return replies < expected;
181         }
182     }
183 
184     DoneCondition cond;
185 
186     foreach (cmd; UserFileRange(env.compileDb, env.files, env.compiler.extraFlags, env.flagFilter)) {
187         if (cmd.isNull) {
188             result_.status = Status.failed;
189             result_.score -= 100;
190             result_.msg ~= Msg(MsgSeverity.failReason,
191                     "clang-tidy where unable to find one of the specified files in compile_commands.json");
192             break;
193         }
194 
195         cond.expected++;
196 
197         immutable(TidyWork)* w = () @trusted{
198             return cast(immutable) new TidyWork(tidyArgs, cmd.absoluteFile, !env.logg.toFile);
199         }();
200         auto t = task!taskTidy(thisTid, w);
201         pool.put(t);
202     }
203 
204     while (cond.isWaitingForReplies) {
205         () @trusted{
206             try {
207                 if (receiveTimeout(1.dur!"seconds", &handleResult)) {
208                     cond.replies++;
209                 }
210             } catch (Exception e) {
211                 logger.error(e.msg);
212             }
213         }();
214     }
215 }
216 
217 /// Run clang-tidy with to fix the code.
218 void executeFixit(Environment env, string[] tidyArgs, ref Result result_) {
219     import std.algorithm : copy, map;
220     import std.array : array;
221     import std.path : buildPath;
222     import std.process : spawnProcess, wait;
223     import code_checker.compile_db : UserFileRange, CompileCommandFilter;
224     import code_checker.engine.logger : Logger;
225 
226     AbsolutePath[] files;
227     auto logg = Logger(env.logg.dir);
228 
229     if (env.logg.toFile) {
230         logg.setup;
231         tidyArgs ~= ["-export-fixes", buildPath(env.logg.dir, "fixes.yaml")];
232     }
233 
234     foreach (cmd; UserFileRange(env.compileDb, env.files, null, CompileCommandFilter.init)) {
235         if (cmd.isNull) {
236             result_.status = Status.failed;
237             result_.score -= 100;
238             result_.msg ~= Msg(MsgSeverity.failReason,
239                     "clang-tidy where unable to find one of the specified files in compile_commands.json");
240             break;
241         }
242         files ~= cmd.absoluteFile;
243     }
244 
245     auto args = tidyArgs ~ files.map!(a => cast(string) a).array;
246     logger.tracef("run: %s", args);
247 
248     auto status = spawnProcess(args).wait;
249     if (status != 0) {
250         result_.status = Status.failed;
251         result_.score -= 1000;
252         result_.msg ~= Msg(MsgSeverity.failReason, "clang-tidy failed to apply fixes");
253     }
254 }
255 
256 struct TidyResult {
257     AbsolutePath file;
258     CountErrorsResult errors;
259 
260     /// Exit status from running clang tidy
261     int clangTidyStatus;
262 
263     /// Output to the user
264     string[] output;
265 
266     void print() @safe nothrow const scope {
267         import std.ascii : newline;
268         import std.stdio : writeln;
269 
270         foreach (l; output)
271             writeln(l).collectException;
272     }
273 }
274 
275 struct TidyWork {
276     string[] args;
277     AbsolutePath p;
278     bool useColors;
279 }
280 
281 void taskTidy(Tid owner, immutable TidyWork* work_) nothrow @trusted {
282     import std.algorithm : copy;
283     import std.array : appender;
284     import std.concurrency : send;
285     import std.format : format;
286     import code_checker.engine.builtin.clang_tidy_classification : mapClangTidy,
287         Severity, color;
288 
289     auto tres = new TidyResult;
290     TidyWork* work = cast(TidyWork*) work_;
291 
292     try {
293         string diagMsg(Severity s, string diag) {
294             tres.errors.put(s);
295             if (work.useColors)
296                 return format("%s[%s]", diag, color(s));
297             return format("%s[%s]", diag, s);
298         }
299 
300         tres.file = work.p;
301 
302         auto res = runClangTidy(work.args, [work.p]);
303         tres.clangTidyStatus = res.status;
304 
305         auto app = appender!(string[])();
306         mapClangTidy!diagMsg(res.stdout, app);
307 
308         res.stderr.copy(app);
309 
310         tres.output = app.data;
311     } catch (Exception e) {
312         logger.warning(e.msg).collectException;
313     }
314 
315     while (true) {
316         try {
317             owner.send(cast(immutable) tres);
318             break;
319         } catch (Exception e) {
320             logger.tracef("failed sending to: %s", owner).collectException;
321         }
322     }
323 }
324 
325 struct ClangTidyConstants {
326     static immutable confFile = ".clang-tidy";
327 }
328 
329 auto runClangTidy(string[] tidy_args, AbsolutePath[] fname) {
330     import std.algorithm : copy;
331     import std.array : appender;
332     import code_checker.process;
333 
334     auto app = appender!(string[])();
335     tidy_args.copy(app);
336     fname.copy(app);
337 
338     return run(app.data);
339 }